#!/usr/bin/python
# -*- coding: utf-8 -*-
"""

================================================================================

    mingus - Music theory Python package, note containers.
    Copyright (C) 2008-2009, Bart Spaans

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

================================================================================
"""

from Note import Note
from mingus.core import intervals, chords, progressions
from mt_exceptions import UnexpectedObjectError


class NoteContainer:

    """The !NoteContainer provides a container for the [refMingusContainersNote \
mingus.containers.Note] objects. It can be used to store single and multiple \
notes and is required for working with [refMingusContainersBar Bars]."""

    notes = []

    def __init__(self, notes=[]):
        self.empty()
        self.add_notes(notes)

    def empty(self):
        """Empties the container."""

        self.notes = []

    def add_note(
        self,
        note,
        octave=None,
        dynamics={},
        ):
        """Adds a note to the container and sorts the notes from low to high. The \
note can either be a string, in which case you could also use the octave \
and dynamics arguments, or a Note object."""

        if type(note) == str:
            if octave is not None:
                note = Note(note, octave, dynamics)
            elif len(self.notes) == 0:
                note = Note(note, 4, dynamics)
            else:
                if Note(note, self.notes[-1].octave) < self.notes[-1]:
                    note = Note(note, self.notes[-1].octave + 1, dynamics)
                else:
                    note = Note(note, self.notes[-1].octave, dynamics)
        if not hasattr(note, 'name'):
            raise UnexpectedObjectError, \
                "Object '%s' was not expected. Expecting a mingus.containers.Note object."\
                 % note
        if note not in self.notes:
            self.notes.append(note)
            self.notes.sort()
        return self.notes

    def add_notes(self, notes):
        """Feeds notes to self.add_note. The notes can either be an other \
!NoteContainer, a list of [refMingusContainersNote Note] objects or \
strings or a list of lists formatted like this:
{{{
notes = [[\"C\", 5], [\"E\", 5], [\"G\", 6]]
}}}
    or even:
{{{
notes = [[\"C\", 5, {\"volume\" : 20}], [\"E\", 6, {\"volume\":20}]]
}}}"""

        if hasattr(notes, 'notes'):
            for x in notes.notes:
                self.add_note(x)
            return self.notes
        elif hasattr(notes, 'name'):
            self.add_note(notes)
            return self.notes
        elif type(notes) == str:
            self.add_note(notes)
            return self.notes
        for x in notes:
            if type(x) == list and len(x) != 1:
                if len(x) == 2:
                    self.add_note(x[0], x[1])
                else:
                    self.add_note(x[0], x[1], x[2])
            else:
                self.add_note(x)
        return self.notes

    def from_chord(self, shorthand):
        """Shortcut to from_chord_shorthand."""

        return self.from_chord_shorthand(shorthand)

    def from_chord_shorthand(self, shorthand):
        """Empties the container and adds the notes in the shorthand. See \
mingus.core.chords.from_shorthand for an up to date list of recognized \
format.
{{{
>>> nc = NoteContainer()
>>> nc.from_chord_shorthand(\"Am\")
['A-4', 'C-5', 'E-5']
}}}"""

        self.empty()
        self.add_notes(chords.from_shorthand(shorthand))
        return self

    def from_interval(
        self,
        startnote,
        shorthand,
        up=True,
        ):
        """Shortcut to from_interval_shorthand."""

        return self.from_interval_shorthand(startnote, shorthand, up)

    def from_interval_shorthand(
        self,
        startnote,
        shorthand,
        up=True,
        ):
        """Empties the container and adds the note described in the startnote and \
shorthand. See core.intervals for the recognized format.
{{{
>>> nc = NoteContainer()
>>> nc.from_interval_shorthand(\"C\", \"5\")
['C-4', 'G-4']
>>> nc.from_interval_shorthand((\"C\", \"5\", False)
['F-3', 'C-4']
}}}"""

        self.empty()
        if type(startnote) == str:
            startnote = Note(startnote)
        n = Note(startnote.name, startnote.octave, startnote.dynamics)
        n.transpose(shorthand, up)
        self.add_notes([startnote, n])
        return self

    def from_progression(self, shorthand, key='C'):
        """Shortcut to from_progression_shorthand."""

        return self.from_progression_shorthand(shorthand, key)

    def from_progression_shorthand(self, shorthand, key='C'):
        """Empties the container and adds the notes described in the progressions \
shorthand (eg. 'IIm6', 'V7', etc). See mingus.core.progressions for all \
the recognized format.
{{{
>>> nc = NoteContainer()
>>> nc.from_progression_shorthand(\"VI\")
['A-4', 'C-5', 'E-5']
}}}"""

        self.empty()
        chords = progressions.to_chords(shorthand, key)

                # warning Throw error, not a valid shorthand

        if chords == []:
            return False
        notes = chords[0]
        self.add_notes(notes)
        return self

    def _consonance_test(self, testfunc, param=None):
        """Private function used for testing consonance/dissonance."""

        n = list(self.notes)
        while len(n) > 1:
            first = n[0]
            for second in n[1:]:
                if param is None:
                    if not testfunc(first.name, second.name):
                        return False
                else:
                    if not testfunc(first.name, second.name, param):
                        return False
            n = n[1:]
        return True

    def is_consonant(self, include_fourths=True):
        """Tests whether the notes are consonants. See the core.intervals module \
for a longer description on consonance."""

        return self._consonance_test(intervals.is_consonant, include_fourths)

    def is_perfect_consonant(self, include_fourths=True):
        """Tests whether the notes are perfect consonants. See the core.intervals \
module for a longer description on consonance."""

        return self._consonance_test(intervals.is_perfect_consonant,
                                     include_fourths)

    def is_imperfect_consonant(self):
        """Tests whether the notes are imperfect consonants. See the core.intervals \
module for a longer description on consonance."""

        return self._consonance_test(intervals.is_imperfect_consonant)

    def is_dissonant(self, include_fourths=False):
        """Tests whether the notes are dissonants. See the core.intervals module \
for a longer description."""

        return not self.is_consonant(not include_fourths)

    def remove_note(self, note, octave=-1):
        """Removes note from container. The note can either be a \
[refMingusContainersNote Note] object or a string representing the \
note's name. If no specific octave is given, the note gets removed in \
every octave."""

        res = []
        for x in self.notes:
            if type(note) == str:
                if x.name != note:
                    res.append(x)
                else:
                    if x.octave != octave and octave != -1:
                        res.append(x)
            else:
                if x != note:
                    res.append(x)
        self.notes = res
        return res

    def remove_notes(self, notes):
        """Removes notes from the containers. This function accepts a list of \
[refMingusContainersNote Note] objects or notes as strings and also \
single strings or [refMingusContainersNote Note] objects."""

        if type(notes) == str:
            return self.remove_note(notes)
        elif hasattr(notes, 'name'):
            return self.remove_note(notes)
        else:
            map(lambda x: self.remove_note(x), notes)
            return self.notes

    def remove_duplicate_notes(self):
        """A helper function that removes duplicate and enharmonic notes from the \
container"""

        res = []
        for x in self.notes:
            if x not in res:
                res.append(x)
        self.notes = res
        return res

    def sort(self):
        """Sorts the notes in the container from low to high."""

        self.notes.sort()

    def augment(self):
        """Augments all the notes in the !NoteContainer"""

        for n in self.notes:
            n.augment()

    def diminish(self):
        """Diminishes all the notes in the !NoteContainer"""

        for n in self.notes:
            n.diminish()

    def to_minor(self):
        """Converts all the notes in the container to their minor equivalent."""

        for n in self.notes:
            n.to_minor()

    def to_major(self):
        """Converts all the notes in the container to their major equivalent."""

        for n in self.notes:
            n.to_major()

    def determine(self, shorthand=False):
        """Determines the type of chord or interval currently in the container"""

        return chords.determine(self.get_note_names(), shorthand)

    def transpose(self, interval, up=True):
        """Transposes all the notes in the container up or down the given interval."""

        for n in self.notes:
            n.transpose(interval, up)
        return self

    def get_note_names(self):
        """Returns a list with all the note names in the current container. Every \
name will only be mentioned once."""

        res = []
        for n in self.notes:
            if n.name not in res:
                res.append(n.name)
        return res

    def __repr__(self):
        """A nice and clean string representation of the note container"""

        return str(self.notes)

    def __getitem__(self, item):
        """This method allows you to use the container as a simple array.
    Example:
{{{
>>> n = NoteContainer([\"C\", \"E\", \"G\"])
>>> n[0]
'C-4'
}}}
"""

        return self.notes[item]

    def __setitem__(self, item, value):
        """This allows you to use [] notation on !NoteContainers. This function \
accepts Notes and notes as string.
    Examples:
{{{
>>> n = NoteContainer([\"C\", \"E\", \"G\"])
>>> n[0] = \"B\"
[\"B-5\", \"E-5\", \"G-5\"]
}}}"""

        if type(value) == str:
            n = Note(value)
            self.notes[item] = n
        else:
            self.notes[item] = value
        return self.notes

    def __add__(self, notes):
        """This method allows you to use '+' notation on !NoteContainers.
    Example:
{{{
>>> n = NoteContainer([\"C\", \"E\", \"G\"])
>>> n + \"B\"
[\"C-4\", \"E-4\", \"G-4\", \"B-4\"]
}}}"""

        self.add_notes(notes)
        return self

    def __sub__(self, notes):
        """This method allows you to use the '-' operator on !NoteContainers.
    Example:
{{{
>>> n = NoteContainer([\"C\", \"E\", \"G\"])
>>> n - \"E\"
[\"C-4\", \"G-4\"]
}}}"""

        self.remove_notes(notes)
        return self

    def __len__(self):
        """Returns the number of notes in the container"""

        return len(self.notes)

    def __eq__(self, other):
        """Overloads the == operator for !NoteContainer instances."""

        for x in self:
            if x not in other:
                return False
        return True


